Functional Programming in F#

Part II — Lexers & Parsers

Steven Gilham

Based on material developed in collaboration with Will Harwood and Laurence Jordan and presented in 2008

Course Overview

  • Introduction to Functional Programming via Circuits
  • Lexing and Parsing
    • Today's presentation
  • Interpreters
  • Distributed and Concurrent Programming
  • Advanced topics
    • Continuations, monads & computations, etc.
  • Non-functional bits
    • Objects, Exceptions, Libraries, .net, type providers etc.

Lexing and Parsing

A standard application of the techniques

Lexers and Parsers

  • A simple language
    • and its syntax
  • Lexing
  • Parsing
  • Transforms
    • And generating syntax trees

A Simple Language – LISP

Syntax

NOT a LISP course in your FP course!

Expressions are of the form

(mult (add x 5) factor)
(bind ((x. 1) (y . 2) (z . 3)) (add x y z)) 
(defun append (x y) 
        (if (null x) y 
             (cons (head x) (append (tail x) y))))

Lexer

Lexer takes character data and presents a stream of tokens

Parser

Parser takes the token stream and creates a syntax tree

Lexers

Basic Lexer

A state machine

String to character list

"abcde" -> [ 'a'; 'b'; 'c'; 'd'; 'e' ]

"abcde"
[ 'a' ] "bcde"
[ 'a'; 'b' ] "cde"
 ...
[ 'a'; 'b'; 'c'; 'd'; 'e' ] ""
let rec toList (i : string) = 
    if i.Length > 0 
      then ((i.Chars(0))::(toList (i.Substring(1))))
      else [];;

Concept

If we had written

let rec toList i = … 

we would get the following from e.g i.Length

Lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed prior to this program point to constrain the type of the object. This may allow the lookup to be resolved.

= The type of i cannot be inferred.

Digression – Set-up for exercises

We are, in this lecture, working from first principles throughout, rather than going to library code — the availability of mature facilities such as fslex/fsyacc or FParsec would reduce this example into just the incantations to initialise those.

This toList implementation is intended as a reminder of the technique used, without diverting into implementation detail. We can see that the repeated creation of temporary strings, for example, would be a potential performance issue in practice. Indeed, what one might write in practice would be more like

let toList s = (s :> char seq) |> Seq.toList;;

but objects and modules come later in the series.

Lexical classes

type lexical = BRA | KET | DOT | SYMATOM of char list | NUMATOM of int ;;
  • Separators are '(', ')' '.' and whitespace
    • Called stop cases in the code.
let stopcases = toList "().\n\t " ;;

Recall

An Algebraic Data Type

Predicates

Test what we have found

Is Member of List?
let rec mem x l = 
  match l with
  | [] -> false
  | (y::ys) -> if x = y 
                 then true 
                 else mem x ys ;;
Is Stopcase?
let stopcase x =  mem x stopcases ;;
Define digits
let digits = toList "1234567890" ;;
Is Digit?
let digit x = mem  x digits ;;
Is Alpha?
let alpha x = not(mem  x  (digits @ stopcases)) ;;

Character list to integer

char → int

char list → int

let digitToint i = 
   match i with
   | '0' -> 0
   | '1' -> 1
   | '2' -> 2
   | '3' -> 3
   | '4' -> 4
   | '5' -> 5
   | '6' -> 6
   | '7' -> 7
   | '8' -> 8
   | '9' -> 9
   | _ -> 0 ;;
let mkint s =
 let rec mkint' s a =
   match s with 
   |[] -> a
   | (x::xs) ->  mkint' xs ((a*10) + (digitToint x))
 mkint' s 0;;

Component Lexers

Symbol lexer
let rec lexSym s l =
  match l with 
  |  [] -> (SYMATOM s, [])
  | (x::xs) -> if stopcase x 
               then (SYMATOM s, x::xs) 
               else lexSym (s@[x]) xs ;;
Number or Symbol
let rec lexNumOrSym s l =
 match l with
    |[]-> (NUMATOM(mkint s), []) 
    |(x::xs) -> 
        (if (stopcase x) then ( NUMATOM(mkint s), x::xs)
        else if (alpha x) then lexSym (s@[x]) xs
        else lexNumOrSym  (s@[x]) xs ) ;;

The Complete Lexer

Assembling the pieces

  • Skip whitespace
  • '(', ')' and '.' go to the corresponding token
  • Symbols and numbers as above
let rec lexer l = 
  match l with
  | []       -> []
  | '\n'::xs -> lexer xs
  | ' '::xs  -> lexer xs
  | '\t'::xs -> lexer xs
  | '('::xs  -> BRA::(lexer xs)
  | ')'::xs  -> KET::(lexer xs)
  | '.'::xs  -> DOT::(lexer xs)
  | x::xs    -> 
    let (l,r) = if alpha x 
                then (lexSym [x]  xs)
                else (lexNumOrSym [x] xs)
    l :: lexer r;;

Exercise 2.1

Write a lexer for .ica/.ini files (see also Exercise 2.3 below)

Parsers

The Language

Define the grammar it obeys

  • SEXPR = ATOM | BRA SEXPR DOT SEXPR KET | BRA SEXPR* KET
  • ATOM = SYMBOL | NUMBER
  • → SEXPR = SYMBOL | NUMBER | ( SEXPR . SEXPR ) | ( SEXPR* )

Our Approach

Translate the grammar directly into a program by

  • Creating elementary parsers for bra, ket, dot, symbols, atoms etc.
  • Combining by sequence '>>', alternation '||' and iteration 'ITERATE'
let parseAtom  s = ((parseSym >* ATOMACTION) ||  (parseNum >* ATOMACTION)) s ;;

let rec parseSEXPR s = 
  (parseAtom ||   
   (bra >> parseSEXPR >> dot >> parseSEXPR >> ket >* CONSPAIR) ||
   (bra >>  (ITERATE parseSEXPR) >* CONSLIST >> ket)
  ) s;;

Concept

  • Code references go up the file (bottom up)
  • Grammars can reference down the file (top down)

Concept

  • A parse state is the input yet to be parsed plus the results so far
  • A parser takes a parse state, works on the input, and returns an updated state or a failure

Basic Parsers

Capture the concept of success or failure
type 'a Maybe = Return of 'a | Fail;;
A simple parser that takes a list of lexed items and either returns a new parse state or fails
let parseSym  s =
 match s with
 | Return((SYMATOM y)::ys, (_)) ->
       Return(ys,[SYMATOM y]) 
 | _ ->  Fail ;;
Example (mult 1 2) where '(' has been read...
( [SYMATOM mult ; NUMATOM 1; NUMATOM 2; KET] , ANYTHING) →
  (  [NUMATOM 1; NUMATOM 2]; KET , [SYMATOM mult])

Concept

  • Maybe is a standard pattern
  • Corresponds to the OO "null object" pattern
  • So, no null or need for null
  • Generalises e.g. Haskell-style
    type Either = Left of 'error
    	      | Right of 'result
  • This is Separation of Concerns, FP style

Basic Parsers, continued

Again, but for numbers
let parseNum  s =
 match s with
 | Return((NUMATOM y)::ys, a) ->  Return(ys,[NUMATOM y]) 
 | (_) ->  Fail ;;
Helper function for constants – a higher order parser that takes the token to match as a parameter
let parseConst x s =
  match s with 
  | Return(y::ys, a) -> if x = y then Return(ys, []) else Fail 
  | _ ->  Fail ;;
Parsers for the lexical constants BRA, KET and DOT.
let bra x = parseConst BRA x;;

let ket x = parseConst KET x;;

let dot x = parseConst DOT x;;

Concept

Partial function application

val parseConst :
  x:'a -> s:('a list * 'b) Maybe -> ('a list * 'c list) Maybe
    when 'a : equality

leads to

val bra : x:(lexical list * 'a) Maybe -> 
		(lexical list * 'b list) Maybe

Combining Parser Primitives — Alternation

"Either X or Y"

Try f. If it fails, try g
let OTHERWISE f g  x =
  let result = f x
  if result = Fail then (g x) else result ;;
Make it an infix
let (||) = OTHERWISE;;

Combining Parser Primitives — Sequence

"After X then Y"

Try f. If it fails, the parse fails.
If it succeeds, try g on the new parse state; if that fails, the parse fails.
let THENDO  f g x =
  let u = f x
  match u with
  | Return(rest1, result1) 
               -> let v = g u
                  match v with
                  | Return(rest2, result2) -> 
                    Return(rest2, result1 @ result2)
                  | Fail-> Fail
  | Fail -> Fail ;;
Make it an infix
let (>>) = THENDO;;

Combining Parser Primitives — Iteration

"As many X as are available"

Try f. If it fails, the parse is a no-op.
If it succeeds, recurse on the new parse state.
let rec ITERATE f x = 
  let u = f x
  match u with 
  | Fail -> x
  | Return(rest2, result2) -> 
           match x with
           | Fail -> Fail
           | Return(rest1, result1) -> 
               ITERATE f (Return(rest2, result1 @ result2));;
Not an infix  

Exercise 2.2

Write the operator OPT such that OPT A matches zero or one occurrence of the parser A

Write the parser PLUS such that PLUS A matches one or more occurrences of A

Transforms

Transforms

This builds the syntax tree

The parsers only check the structure of the expression

We also want them to build an abstract structure corresponding to the string!

SEXPR = ATOM → ATOM-ACTION |
        BRA SEXPR DOT SEXPR KET → PAIR-ACTION |
        BRA SEXPR* KET → LIST-ACTION
ATOM = SYMBOL → SYMBOL-ACTION |
       NUMBER → NUMBER-ACTION

Transforms take a parse state and build a parse state of a different type

(unparsed input, parse items list) -> (unparsed input, LISP Tree list)

LISP trees are built from ATOM, NIL and CONS elements

Our Abstract Syntax Tree

type SEXPR = SYMBOL of char list | NUMBER of int | NIL | CONS of SEXPR * SEXPR ;;

Transforms continued

Parse input using f, then transform according to h.
let TRANSFORM f h s =
  let u = f s
  match u with
  | Return(rest, result) 
               ->  Return(rest, h result)
  | Fail -> Fail ;;
Make it an infix
let (>*) = TRANSFORM;;

Grammar with Actions : A refinement

The representation we seek is

SEXPR = ATOM |
        BRA SEXPR DOT SEXPR KET → CONSPAIR |
        BRA SEXPR* → CONSLIST KET
ATOM = SYMBOL → ATOM-ACTION |
       NUMBER → ATOM-ACTION

Actions : From grammar to code

CONSPAIR – produce a singleton list containing the CONS of the items.
let CONSPAIR [ x; y ] = [CONS(x,y)];;
CONSLIST – produce a CONS representing a list
let CONSLIST l =
 let rec conslist y =
   match y with
   | x::xs -> CONS(x, conslist xs)
   | [] -> NIL 
 [conslist l];;
ATOMACTION – translate base cases between data types
let ATOMACTION v  =
  match v with
  | [SYMATOM u] ->  [SYMBOL u] 
  | [NUMATOM i] ->  [NUMBER i]
  | _ ->  [];;

The Complete Parser

Recapitulating

let parseAtom  s = ((parseSym >* ATOMACTION) ||  (parseNum >* ATOMACTION)) s ;;

let rec parseSEXPR s = 
  (parseAtom ||   
   (bra >> parseSEXPR >> dot >> parseSEXPR >> ket >* CONSPAIR) ||
   (bra >>  (ITERATE parseSEXPR) >* CONSLIST >> ket)
  ) s;;

Wrapping the Parser

Building a usable API

let parse str = 
  let r = parseSEXPR (Return (lexer (toList str), [])) 
  match r with 
  | Return(_ , result::_) -> result
  | Return(_, []) -> NIL
  | Fail -> NIL ;;

Test Case

parse "( ((ABC)) . (123. 456))";;

Result

val it : SEXPR =
  CONS
    (CONS (CONS (SYMBOL ['A'; 'B'; 'C'],NIL),NIL),CONS (NUMBER 123,NUMBER 456))

Exercise 2.3a

Define a grammar

File = Line*

Line = WHITESPACE | CMD | 'NL'

WHITESPACE = ('NL' | 'TAB' | 'SPACE' )

CMD = SectionHeader | BINDING | COMMENT

SectionHeader = '[' HeaderString ']'

BINDING = BindString '=' BindString

HeaderString = Any but ('[' | ']' | 'NL')

BindString = Quote | Any but ('='| 'NL' )

Quote = '"' (Anything but '"') '"'

COMMENT = ';' (Any but 'NL') 'NL'

Exercise 2.3b

Write a Lexer for the grammar

Write a Parser for it

Extend it, using the full INI file format definition.

What real-world performance implications are there in the use of this sort of parser? Re-implement one of the parsers to avoid the limitations of the strictly evaluated list datatype.

Wrapping up

つづく